package com.xxx.base.common.region;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.xxx.base.util.Utils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Description：
 * Created by fyx on 4/25/2017.
 * Version：
 */
public class RegionUtils {
    private static List<Region> provinces;
    private static Map<String, Region> regionMap;
    private static List<Region> platRegions;
    private static List<Region> cities;
    private static List<Region> districts;
    private static Pattern patDefaultSplit = Pattern.compile("(?<prefix>^.{2})(?<suffix>.*)$");

    static {
        provinces = new ArrayList<>();
        cities = new ArrayList<>();
        districts = new ArrayList<>();
        regionMap = new HashMap<>();
        platRegions = new ArrayList<>();
        InputStream regionsStream = RegionUtils.class.getResourceAsStream("/regions.json");
        InputStream appendixStream = RegionUtils.class.getResourceAsStream("/regions-appendix.json");

        String jsonRegionsString = null;
        String jsonAppendixString = null;
        try {
            jsonRegionsString = IOUtils.toString(regionsStream, "UTF-8");
            jsonAppendixString = IOUtils.toString(appendixStream, "UTF-8");
        } catch (IOException e) {
            e.printStackTrace();
        }
        JSONObject jProvinces = JSON.parseObject(jsonRegionsString);
        JSONObject jAppendix = JSON.parseObject(jsonAppendixString);
        JSONObject jAppendAliax = jAppendix.getJSONObject("append-alias");
        JSONObject jAppendRegions = jAppendix.getJSONObject("append-regions");
        jProvinces.keySet().forEach(new Consumer<String>() {
            @Override
            public void accept(String code) {
                JSONObject jProvince = jProvinces.getJSONObject(code);
                Region province = new Region();
                province.setCode(code);
                province.setVisible(true);
                province.setName(jProvince.getString("name"));
                province.setLevel(RegionLevel.PROVINCE);
                province.setParentRegion(null);
                province.setSubRegions(new ArrayList<>());
                JSONObject jCities = jProvince.getJSONObject("subRegions");
                jCities.keySet().forEach(new Consumer<String>() {
                    @Override
                    public void accept(String code) {
                        JSONObject jCity = jCities.getJSONObject(code);
                        Region city = new Region();
                        city.setCode(code);
                        city.setName(jCity.getString("name"));
                        city.setLevel(RegionLevel.CITY);
                        city.setVisible(jCity.getBoolean("visible"));
                        city.setParentRegion(province);
                        province.getSubRegions().add(city);
                        regionMap.put(city.getCode(), city);
                        JSONObject jDistricts = jCity.getJSONObject("subRegions");
                        jDistricts.keySet().forEach(new Consumer<String>() {
                            @Override
                            public void accept(String code) {
                                JSONObject jDistrict = jDistricts.getJSONObject(code);
                                Region district = new Region();
                                district.setCode(code);
                                district.setName(jDistrict.getString("name"));
                                district.setLevel(RegionLevel.DISTRICT);
                                district.setVisible(jDistrict.getBoolean("visible"));
                                district.setParentRegion(city);
                                city.getSubRegions().add(district);
                                regionMap.put(district.getCode(), district);
                            }
                        });
                    }
                });
                regionMap.put(province.getCode(), province);
            }
        });

        for (String key : jAppendAliax.keySet()) {
            if (regionMap.containsKey(key)) {
                regionMap.get(key).getAlias().addAll(jAppendAliax.getJSONObject(key).getJSONArray("alias").stream().map(new Function<Object, String>() {
                    @Override
                    public String apply(Object o) {
                        return o.toString();
                    }
                }).collect(Collectors.toList()));
            }
        }
        for (String key : jAppendRegions.keySet()) {
            if (regionMap.containsKey(key)) {
                Region r = regionMap.get(key);
                combineToRegion(jAppendRegions.getJSONObject(key), r.getParentRegion(), r);
            }
        }
        platRegions.addAll(regionMap.values());
        provinces.addAll(regionMap.values().stream().filter(new Predicate<Region>() {
            @Override
            public boolean test(Region region) {
                return region.getLevel().equals(RegionLevel.PROVINCE);
            }
        }).collect(Collectors.toList()));
        cities.addAll(regionMap.values().stream().filter(new Predicate<Region>() {
            @Override
            public boolean test(Region region) {
                return region.getLevel().equals(RegionLevel.CITY);
            }
        }).collect(Collectors.toList()));
        districts.addAll(regionMap.values().stream().filter(new Predicate<Region>() {
            @Override
            public boolean test(Region region) {
                return region.getLevel().equals(RegionLevel.DISTRICT);
            }
        }).collect(Collectors.toList()));
    }

    public static List<Region> getProvinces() {
        return provinces;
    }

    private static void combineToRegion(JSONObject jRegion, Region parentRegion, Region region) {
        if (jRegion.containsKey("name")) {
            region.setName(jRegion.getString("name"));
        }
        if (parentRegion != null)
            region.setLevel(RegionLevel.parseIndex(parentRegion.getLevel().getIndex() + 1));
        else
            region.setLevel(RegionLevel.PROVINCE);
        if (jRegion.containsKey("visible")) {
            region.setVisible(jRegion.getBoolean("visible"));
        }
        region.setParentRegion(parentRegion);
        if (parentRegion != null) {
            if (parentRegion.getSubRegions().stream().noneMatch(new Predicate<Region>() {
                @Override
                public boolean test(Region r) {
                    return r.getCode().equals(region.getCode());
                }
            })) {
                parentRegion.getSubRegions().add(region);
            }
        }
        JSONObject jSubRegions = jRegion.getJSONObject("subRegions");
        if (jSubRegions != null) {
            for (String key : jSubRegions.keySet()) {
                Region r;
                if (regionMap.containsKey(key)) {
                    r = regionMap.get(key);
                } else {
                    r = new Region();
                    r.setCode(key);
                }
                combineToRegion(jSubRegions.getJSONObject(key), region, r);
            }
        }
    }

    public static Region getByCode(String code) {
        return regionMap.get(code);
    }

    public static Region getByName(String input) {
        if (input.length() < 2) {
            return null;
        }
        List<Region> founds = findRegion(input, platRegions, patDefaultSplit);
        if (founds.isEmpty()) {
            return null;
        }
        Optional<Region> optional = founds.stream().findAny();
        if (optional.isPresent())
            return optional.get();
        else
            return null;
    }

    private static List<Region> findRegion(String input, List<Region> fromRegions, Pattern pattern) {
        Matcher matcher = pattern.matcher(input);
        if (matcher.find()) {
            String prefix = matcher.group("prefix");
            String inputSuffi = matcher.group("suffix");
            return fromRegions.stream().filter(new Predicate<Region>() {
                @Override
                public boolean test(Region region) {
                    try {
                        boolean matched = region.getName().startsWith(prefix);
                        boolean matchedWithAlias = region.getAlias().stream().anyMatch(new Predicate<String>() {
                            @Override
                            public boolean test(String s) {
                                return s.startsWith(prefix);
                            }
                        });
                        return matched || matchedWithAlias;
                    } catch (NullPointerException e) {
                        return false;
                    }
                }
            }).sorted(new Comparator<Region>() {
                @Override
                public int compare(Region o1, Region o2) {
                    int matchRate = calcMatchRate(o2.getName(), inputSuffi) - calcMatchRate(o1.getName(), inputSuffi);
                    int lengthRate = matchRate != 0 ? matchRate : o1.getName().length() - o2.getName().length();
                    int levelRate = lengthRate != 0 ? lengthRate : o1.getLevel().getIndex() - o2.getLevel().getIndex();
                    return levelRate;
                }
            }).collect(Collectors.toList());
        } else {
            return new ArrayList<>();
        }
    }

    public static Region getByNamesChain(String inputProvinceName, String inputCityName, String inputDistrictName) {

//        if (calcMatchRate(inputCityName, inputDistrictName) >= 2) {
//            inputDistrictName = "";
//        }

        if (inputProvinceName.startsWith("北京") && inputCityName.startsWith("北京") ||
                inputProvinceName.startsWith("上海") && inputCityName.startsWith("上海") ||
                inputProvinceName.startsWith("天津") && inputCityName.startsWith("天津") ||
                inputProvinceName.startsWith("重庆") && inputCityName.startsWith("重庆")) {
            inputCityName = null;
        }
        List<Region> foundProvinces = findRegion(inputProvinceName, provinces, patDefaultSplit);
        if (StringUtils.isEmpty(inputCityName) && StringUtils.isEmpty(inputDistrictName)) {
            Optional<Region> optionalProvince = foundProvinces.stream().findAny();
            if (optionalProvince.isPresent())
                return optionalProvince.get();
            else
                return null;
        }

        if (StringUtils.isEmpty(inputDistrictName)) {
            List<Region> childCities = provinces.stream().flatMap(new Function<Region, Stream<Region>>() {
                @Override
                public Stream<Region> apply(Region region) {
                    return region.getSubRegions().stream();
                }
            }).collect(Collectors.toList());
            List<Region> foundCities = findRegion(inputCityName, childCities, patDefaultSplit);
            if (foundCities.isEmpty()) {
                List<Region> childDistricts = foundProvinces.stream().flatMap(new Function<Region, Stream<Region>>() {
                    @Override
                    public Stream<Region> apply(Region region) {
                        return region.getSubRegions().stream().flatMap(new Function<Region, Stream<? extends Region>>() {
                            @Override
                            public Stream<? extends Region> apply(Region region) {
                                return region.getSubRegions().stream();
                            }
                        });
                    }
                }).collect(Collectors.toList());
                List<Region> foundDistricts = findRegion(inputCityName, childDistricts, patDefaultSplit);
                Optional<Region> optionalDistrict = foundDistricts.stream().findAny();
                if (optionalDistrict.isPresent())
                    return optionalDistrict.get();
                else
                    return null;
            } else {
                return foundCities.stream().findAny().get();
            }
        }

        List<Region> foundDistricts;
        if (!foundProvinces.isEmpty()) {
            List<Region> childDistricts = foundProvinces.stream().flatMap(new Function<Region, Stream<Region>>() {
                @Override
                public Stream<Region> apply(Region region) {
                    return region.getSubRegions().stream().flatMap(new Function<Region, Stream<? extends Region>>() {
                        @Override
                        public Stream<? extends Region> apply(Region region) {
                            return region.getSubRegions().stream();
                        }
                    });
                }
            }).collect(Collectors.toList());
            foundDistricts = findRegion(inputDistrictName, childDistricts, patDefaultSplit);
            if (foundDistricts.isEmpty()) {
                List<Region> childCities = foundProvinces.stream().flatMap(new Function<Region, Stream<? extends Region>>() {
                    @Override
                    public Stream<? extends Region> apply(Region region) {
                        return region.getSubRegions().stream();
                    }
                }).collect(Collectors.toList());
                foundDistricts = findRegion(inputDistrictName, childCities, patDefaultSplit);
                if (foundDistricts.isEmpty()) {
                    foundDistricts = findRegion(inputCityName, childCities, patDefaultSplit);
                }
            }
        } else {
            foundDistricts = findRegion(inputCityName, districts, patDefaultSplit);
        }

        Optional<Region> optionalDistrict = foundDistricts.stream().findAny();
        if (optionalDistrict.isPresent())
            return optionalDistrict.get();
        else
            return null;
    }

    private static int calcMatchRate(String standard, String comparee) {
        int matched = 0;
        for (int i = 0; i < comparee.length(); i++) {
            int position = standard.indexOf(comparee.charAt(i));
            if (position >= 0) {
                matched++;
                standard = standard.substring(position + 1, standard.length());
            }
        }
        return matched;
    }

    public static Set<String> flattenRegionTrees(Set<Region> rootRegions) {
        return rootRegions.stream().map(new Function<Region, Set<String>>() {
            @Override
            public Set<String> apply(Region region) {
                return flattenRegionTree(region);
            }
        }).reduce(new BinaryOperator<Set<String>>() {
            @Override
            public Set<String> apply(Set<String> strings, Set<String> strings2) {
                strings.addAll(strings2);
                return strings;
            }
        }).get();
    }

    public static String getAddressByCode(String code) {
        Region region = getByCode(code);
        if (region == null) return "";
        List<String> names = new ArrayList<>(3);
        names.add(region.getName());

        Region parentRegion = region.getParentRegion();
        while (parentRegion != null) {
            names.add(parentRegion.getName());
            parentRegion = parentRegion.getParentRegion();
        }

        Collections.reverse(names);
        return Utils.join(names, " ");
    }

    private static Set<String> flattenRegionTree(Region rootRegion) {
        Set<String> flatten = new HashSet<>();
        flatten.add(rootRegion.getCode());
        rootRegion.getSubRegions().forEach(new Consumer<Region>() {
            @Override
            public void accept(Region childRegion) {
                flatten.addAll(flattenRegionTree(childRegion));
            }
        });
        return flatten;
    }
}
